/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hugegraph.store.query;

import lombok.Data;
import org.apache.hugegraph.id.Id;
import org.apache.hugegraph.query.ConditionQuery;
import org.apache.hugegraph.store.query.func.AggregationFunctionParam;

import java.util.HashSet;
import java.util.List;

@Data
public class StoreQueryParam {

    /**
     * not Agg：
     * No filtering if null or size == 0
     */
    private final PropertyList properties = PropertyList.of();
    private final boolean groupBySchemaLabel = false;
    private final SORT_ORDER sortOrder = SORT_ORDER.ASC;
    /**
     * Is deduplication of keys required for multiple query parameters or index queries
     */
    private final DEDUP_OPTION dedupOption = DEDUP_OPTION.NONE;
    /**
     * the number of results
     */
    private final Integer limit = 0;
    /**
     * Offset values are currently hosted and managed by the server, with an expected theoretical value of 0
     */
    private final Integer offset = 0;
    /**
     * Sampling rate
     */
    private final double sampleFactor = 1.0;
    /**
     * 从index id中构建 base element。在No scan的case下
     */
    private final boolean loadPropertyFromIndex = false;
    /**
     * Whether to parse TTL
     */
    private final boolean checkTTL = false;
    /**
     * Generated by client，distinguish different queries
     */
    private String queryId;
    /**
     * the graph
     */
    private String graph;
    /**
     * the table name
     */
    private String table;
    /**
     * Aggregation func list
     */
    private List<AggregationFunctionParam> funcList;
    /**
     * Group list, which also serves as properties
     */
    private List<Id> groupBy;
    /**
     * Sorting field
     * Priority lower than property.
     * For Agg queries: ID is invalid if not included in group by clause
     * For non-Agg queries: ID is invalid if not present in properties
     */
    private List<Id> orderBy;
    /**
     * Filtering condition
     */
    private ConditionQuery conditionQuery;
    /**
     * todo not implement now
     */
    private List<Integer> having;
    private StoreQueryType queryType;
    private List<QueryTypeParam> queryParam;
    /**
     * Used in non-order-by, non-aggregation queries
     */
    private byte[] position;
    /**
     * Add corresponding attributes from the OLAP table to the HgElement (Vertex)
     */
    private List<Id> olapProperties;
    /**
     * The index has inner elements in AND relations and outer elements in OR relations.
     * IndexRange represents a range query.
     * If the scanType is INDEX_SCAN, a lookup back to the original table is required.
     */
    private List<List<QueryTypeParam>> indexes;

    private static void isFalse(boolean expression, String message) {

        if (message == null) {
            throw new IllegalArgumentException("message is null");
        }

        if (expression) {
            throw new IllegalArgumentException(message);
        }
    }

    private static <E> boolean isEmpty(List<E> list) {
        return list == null || list.size() == 0;
    }

    public void checkQuery() {
        isFalse(queryId == null, "query id is null");
        isFalse(graph == null, "graph is null");
        isFalse(table == null, "table is null");

        isFalse(queryType == null, "queryType is null");

        isFalse(queryType == StoreQueryType.PRIMARY_SCAN && isEmpty(queryParam),
                "query param is null when PRIMARY_SCAN");
        // no scan & index scan should have indexes
        isFalse(queryType == StoreQueryType.NO_SCAN && isEmpty(indexes),
                "ScanType.NO_SCAN without indexes");
        isFalse(queryType == StoreQueryType.NO_SCAN &&
                (indexes.size() != 1 || indexes.get(0).size() != 1),
                "ScanType.NO_SCAN only support one index");
        isFalse(loadPropertyFromIndex &&
                (isEmpty(indexes) || indexes.size() != 1 || indexes.get(0).size() != 1),
                " loadPropertyFromIndex only support one(must be one) index in no scan");

        isFalse(queryType == StoreQueryType.INDEX_SCAN && isEmpty(indexes),
                "ScanType.INDEX_SCAN without indexes ");
        //FIXME is this right?
        isFalse(!isEmpty(groupBy) && !isEmpty(properties.getPropertyIds()) &&
                !new HashSet<>(groupBy).containsAll(properties.getPropertyIds()),
                "properties should be subset of groupBy");

        isFalse(!isEmpty(groupBy) && !isEmpty(orderBy) &&
                !new HashSet<>(groupBy).containsAll(orderBy),
                "order by should be subset of groupBy");

        // isFalse(properties.isEmptyId() && ! queryParam.stream().allMatch(p -> p.isIdScan()),
        //       "empty property only apply id scan");

        // todo: just group by, no aggregations ??
        if (funcList != null) {
            for (var func : funcList) {
                if (func.getFunctionType() == AggregationFunctionParam.AggregationFunctionType.SUM
                    ||
                    func.getFunctionType() == AggregationFunctionParam.AggregationFunctionType.MAX
                    ||
                    func.getFunctionType() == AggregationFunctionParam.AggregationFunctionType.MIN
                    || func.getFunctionType() ==
                       AggregationFunctionParam.AggregationFunctionType.AVG) {
                    isFalse(func.getField() == null,
                            func.getFunctionType().name() + " has no filed value");
                }

                if (func.getFunctionType() ==
                    AggregationFunctionParam.AggregationFunctionType.SUM) {
                    //  ||func.getFunctionType() == AggregationFunctionParam
                    //  .AggregationFunctionType.AVG){
                    isFalse(func.getFieldType() == AggregationFunctionParam.FieldType.STRING,
                            func.getFunctionType().name() + " can not apply a String type");
                }
            }
        }

        isFalse(limit <= 0, "limit should be greater than 0");
        isFalse(sampleFactor < 0 || sampleFactor > 1, "sample factor out of range [0-1]");
    }

    public enum DEDUP_OPTION {
        NONE,
        /**
         * Fuzzy deduplication using bitmap
         */
        DEDUP,
        /**
         * Exact deduplication for the first N rows, approximate for the rest
         */
        LIMIT_DEDUP,
        /**
         * Exact deduplication with guaranteed accuracy
         */
        PRECISE_DEDUP
    }

    public enum SORT_ORDER {
        ASC,
        DESC,
        /**
         * Only for all-ID queries, preserve the original input ID order
         */
        STRICT_ORDER
    }

}
